EventName — название события;
DeviceIDHash — ID пользователя;
EventTimestamp — время события;
ExpId — номер груп для тестов: 246 и 247 — контрольные группы, а 248 — экспериментальная.
import pandas as pd
import matplotlib.pyplot as plt
import datetime as dt
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
import numpy as np
from datetime import datetime
from scipy import stats as st
import math as mth
import cmath
import nbconvert
from IPython.display import Javascript
from nbconvert import HTMLExporter
df = pd.read_csv('logs_exp.csv', sep='\t')
df = df.rename(columns={"EventName": "event", "DeviceIDHash": "device_id", "EventTimestamp": "time_stamp", "ExpId":"test_id"})
# переименовал названия столбцов для удобства
display(df.head(10))
display(df.info())
display(df.isnull().sum())
df = df.drop_duplicates()
#пропусков нет, ушло немного дубликатов, так как всего из было около тысячи для такого массива данных это ок
display(df['event'].value_counts())
display(df['device_id'].value_counts())
display(df['test_id'].value_counts())
df['time_stamp'] = pd.to_datetime(df['time_stamp'], unit='s')
df['date'] = df['time_stamp'].dt.date.astype('datetime64')
df['time'] = df['time_stamp'].dt.time#.astype('datetime64')
# перевели нужные столбцы в даты и добавил стобцы только с датой и только с верменем
display(df['time_stamp'])
display(df.info())
display(df.head())
Подготовили данные к последующей работеа именно:
display(df['event'].value_counts()) #все события
display(df['device_id'].nunique()) # количество уникальных пользователей
display(df.groupby('event').agg({"device_id": "nunique"}).sort_values(
by='device_id', ascending=False)) #смотрим сколько приходится событий на уникальных пользователей
display(df['date'].describe())
#display(df['date'].hist(bins=30))
date_hist = px.histogram(df, x="date",nbins=25,title='распределение действий пользователей по дате',labels={'date':'дата'})
date_hist.show()
Как видно из гистограммы несмотря то что первое дейтсвие у нас было совершенно ещё 25 июля то до августа у практически не было активности, поэтому будем считать от 1 августа включительно, и откинем данные ранее, у нас получится неделя данных
display(df.query('date >="2019-08-01"').info())
#если откинем данные до 1 августа, то всего потеряем около 3 тыс, что совсем немного для такого массива
df = df.query('date >="2019-08-01"')
display(df['test_id'].value_counts())
# также есть все выборки експерементальные, всё ок
display(df.groupby('test_id').agg({"device_id": "nunique"}).sort_values(
by='device_id', ascending=False))
# количество уникальных пользователей в каждой из групп
#
fig = go.Figure(data=[go.Pie(labels=['246','247','248'],
values=[2484,2513,2537])])
fig.show()
Определили временную выборку с 1 августа, так как до 1 августа было мало действий. По сути получили недельную выборку активности пользователей Групы также распределенны верно, разница в выборках меньше 1%, что показывает нам корректность разделения на группы
display(df.groupby('event').agg({"device_id": "nunique"}).sort_values(
by='device_id', ascending=False))
Судя по групировки мы видим что наш пользователе проходит путь: главная страничка - страничка с продуктами - страничка с выбором оплаты и других данных - окно удачной оплаты. Есть ещё окно "помощь", но он не в общей цепочки действий, а точнее скорее всего не всегда в ней. интересно будет посмотреть детальней как это окно влияет на пользователей
users = df.pivot_table(
index='device_id',
columns='event',
values='time_stamp',
aggfunc='min')
display(users.head(15))
# посмтроим воронку с учётом порядка действий пользователей
step_1 = ~users['MainScreenAppear'].isna()
step_2 = step_1 & (users['OffersScreenAppear'] > users['MainScreenAppear'])
step_3 = step_2 & (users['CartScreenAppear'] > users['OffersScreenAppear'])
step_4 = step_3 & (users['PaymentScreenSuccessful'] > users['CartScreenAppear'])
step_5 = step_4 & (users['CartScreenAppear'] > users['Tutorial'])
n_main_page = users[step_1].shape[0]
n_offer = users[step_2].shape[0]
n_cart = users[step_3].shape[0]
n_payment = users[step_4].shape[0]
n_tutorial = users[step_5].shape[0]
print('Зашли на главную страничку:', n_main_page)
print('Зашли на страницу с продуктами:', n_offer)
print('Начали оформлять заказ:', n_cart)
print('Оплатили:', n_payment)
print('Посмотрели справку',n_tutorial )
fig = go.Figure(go.Funnel(
y = ["Зашли на главную страничку", "Зашли на страницу с продуктами", "Начали оформлять заказ", "Оплатили"],
x = [7419, 4201, 1767, 454],
textposition = "inside",
textinfo = "value+percent initial",
opacity = 0.65, marker = {"color": ["deepskyblue", "lightsalmon", "tan", "teal", "silver"],
"line": {"width": [4, 2, 2, 3, 1, 1], "color": ["wheat", "wheat", "blue", "wheat", "wheat"]}},
connector = {"line": {"color": "royalblue", "dash": "dot", "width": 3}})
)
fig.show()
Визуализация воронки показывает показывает следующие тенденции:
Иходя из полученных результатов я бы предварительно выдвинул такие гипотезы и реккомендации:
df_246 = df.query('test_id == "246"')
df_247 = df.query('test_id == "247"')
df_248 = df.query('test_id == "248"')
# делаю для графика funnel
users_246 = df_246.pivot_table(
index='device_id',
columns='event',
values='time_stamp',
aggfunc='min')
users_247 = df_247.pivot_table(
index='device_id',
columns='event',
values='time_stamp',
aggfunc='min')
users_248 = df_248.pivot_table(
index='device_id',
columns='event',
values='time_stamp',
aggfunc='min')
# 246
step_1_246 = ~users_246['MainScreenAppear'].isna()
step_2_246 = step_1_246 & (users_246['OffersScreenAppear'] > users_246['MainScreenAppear'])
step_3_246 = step_2_246 & (users_246['CartScreenAppear'] > users_246['OffersScreenAppear'])
step_4_246 = step_3_246 & (users_246['PaymentScreenSuccessful'] > users_246['CartScreenAppear'])
step_5_246 = step_4_246 & (users_246['CartScreenAppear'] > users_246['Tutorial'])
n_main_page_246 = users_246[step_1_246].shape[0]
n_offer_246 = users_246[step_2_246].shape[0]
n_cart_246 = users_246[step_3_246].shape[0]
n_payment_246 = users_246[step_4_246].shape[0]
n_tutorial_246 = users_246[step_5_246].shape[0]
print('Зашли на главную страничку:', n_main_page_246)
print('Зашли на страницу с продуктами:', n_offer_246)
print('Начали оформлять заказ:', n_cart_246)
print('Оплатили:', n_payment_246)
print('Посмотрели справку',n_tutorial_246)
# 247
step_1_247 = ~users_247['MainScreenAppear'].isna()
step_2_247 = step_1_247 & (users_247['OffersScreenAppear'] > users_247['MainScreenAppear'])
step_3_247 = step_2_247 & (users_247['CartScreenAppear'] > users_247['OffersScreenAppear'])
step_4_247 = step_3_247 & (users_247['PaymentScreenSuccessful'] > users_247['CartScreenAppear'])
step_5_247 = step_4_247 & (users_247['CartScreenAppear'] > users_247['Tutorial'])
n_main_page_247 = users_247[step_1_247].shape[0]
n_offer_247 = users_247[step_2_247].shape[0]
n_cart_247 = users_247[step_3_247].shape[0]
n_payment_247 = users_247[step_4_247].shape[0]
n_tutorial_247 = users_247[step_5_247].shape[0]
print('Зашли на главную страничку:', n_main_page_247)
print('Зашли на страницу с продуктами:', n_offer_247)
print('Начали оформлять заказ:', n_cart_247)
print('Оплатили:', n_payment_247)
print('Посмотрели справку',n_tutorial_247)
# 248
step_1_248 = ~users_248['MainScreenAppear'].isna()
step_2_248 = step_1_248 & (users_248['OffersScreenAppear'] > users_248['MainScreenAppear'])
step_3_248 = step_2_248 & (users_248['CartScreenAppear'] > users_248['OffersScreenAppear'])
step_4_248 = step_3_248 & (users_248['PaymentScreenSuccessful'] > users_248['CartScreenAppear'])
step_5_248 = step_4_248 & (users_248['CartScreenAppear'] > users_248['Tutorial'])
n_main_page_248 = users_248[step_1_248].shape[0]
n_offer_248 = users_248[step_2_248].shape[0]
n_cart_248 = users_248[step_3_248].shape[0]
n_payment_248 = users_248[step_4_248].shape[0]
n_tutorial_248 = users_248[step_5_248].shape[0]
print('Зашли на главную страничку:', n_main_page_248)
print('Зашли на страницу с продуктами:', n_offer_248)
print('Начали оформлять заказ:', n_cart_248)
print('Оплатили:', n_payment_248)
print('Посмотрели справку',n_tutorial_248)
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'группа 246',
y = ["Зашли на главную страничку", "Зашли на страницу с продуктами", "Начали оформлять заказ", "Оплатили"],
x = [2450, 1411, 584, 145],
textinfo = "value+percent initial"))
fig.add_trace(go.Funnel(
name = 'группа 247',
orientation = "h",
y = ["Зашли на главную страничку", "Зашли на страницу с продуктами", "Начали оформлять заказ", "Оплатили"],
x = [2476, 1379, 600, 144],
textposition = "inside",
textinfo = "value+percent previous"))
fig.add_trace(go.Funnel(
name = 'группа 248',
orientation = "h",
y = ["Зашли на главную страничку", "Зашли на страницу с продуктами", "Начали оформлять заказ", "Оплатили"],
x = [2493, 1411, 583, 165],
textposition = "outside",
textinfo = "value+percent total"))
fig.show()
Также для наглядности постролили воронку по разбивке кажой группы
Исходя воронки разбитой по контрольным группам и новой наша конверсия в этих группах более менне равна для всех случаев, но вот у новой группы конверсия в оплату всё таки немного лучше а именно 6,6% от изначальных (против 5.9% и 5.8%) ну и выше по переходу оформление - успешанпя оплата
так как у нас нормальное распределние и мы будем проверять гипотезы о равентсве нашей пропорции среди групп 246 и 247 будем испольщовать z test Фишера Нулевая гипотеза: между долями груп нет стастистической разницы Альтернативаня гипотеза: отвергаем нулевую и есть основания считать, что между долями груп есть разница
# даелаем функицю для проверки гипотез
def a_a_test(data1, data2): #где data1 это количество просмотров от предидушего шага, а data2 количество перезодов на след. уровень
alpha = .1 # критический уровень статистической значимости
p1 = data2[0] / data1[0]
p2 = data2[1] / data1[1]
p_combined = (data2[0] + data2[1]) / (data1[0] + data1[1])
difference = p1 - p2
z_value = difference / cmath.sqrt(p_combined * (1 - p_combined) * (1/data1[0] + 1/data1[1]))
distr = st.norm(0,1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if (p_value < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
проверяем различие долей для перехода главная страница - страница с продуктами
data2 = n_offer_246, n_offer_247
data1 = n_main_page_246, n_main_page_247
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с продуктами - оформление заказа
data2 = n_cart_246, n_cart_247
data1 = n_offer_246, n_offer_247
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с оформление заказа - перход к оплате
data2 = n_payment_246, n_payment_247
data1 = n_cart_246, n_cart_247
display(a_a_test(data1, data2))
проверяем различие долей для перехода главаня страница - перход к справке
data2 = n_tutorial_246, n_tutorial_247
data1 = n_main_page_246, n_main_page_247
display(a_a_test(data1, data2))
проверяем различие долей для перехода оформление заказа - перход к справке
data2 = n_tutorial_246, n_tutorial_247
data1 = n_cart_246, n_cart_247
display(a_a_test(data1, data2))
Наш А/А анализ показал что во всех тестах нет оснований отвергать нулевую гипотезу, и между нашими долями нет статистически значимой разницы. Это говорит нам о том что с нашим разбиением на группы 246 и 247 всё нормально, и то что наше разбиение на группы сработало корректно
Для корректного продолжения експеримента будем продолжать использовать z критерий Фишера. Определим такие гипотезы:
А/В тест груп 266 и 268
проверяем различие долей для перехода главная страница - страница с продуктами
data2 = n_offer_246, n_offer_248
data1 = n_main_page_246, n_main_page_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с продуктами - оформление заказа
data2 = n_cart_246, n_cart_248
data1 = n_offer_246, n_offer_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с оформление заказа - перход к оплате
data2 = n_payment_246, n_payment_248
data1 = n_cart_246, n_cart_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода главаня страница - перход к справке
data2 = n_tutorial_246, n_tutorial_248
data1 = n_main_page_246, n_main_page_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода оформление заказа - перход к справке
data2 = n_tutorial_246, n_tutorial_248
data1 = n_cart_246, n_cart_248
display(a_a_test(data1, data2))
после проведения А/В тестов между контрольной группой 266 и новой 268 можно сказать, что статистически значимой разницы между долями нет, как при 0.1 так и при 0.05 уровню стастистической значимости
А/В тест груп 267 и 268
проверяем различие долей для перехода главная страница - страница с продуктами
data2 = n_offer_247, n_offer_248
data1 = n_main_page_247, n_main_page_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с продуктами - оформление заказа
data2 = n_cart_247, n_cart_248
data1 = n_offer_247, n_offer_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с оформление заказа - перход к оплате
data2 = n_payment_247, n_payment_248
data1 = n_cart_247, n_cart_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода главаня страница - перход к справке
data2 = n_tutorial_247, n_tutorial_248
data1 = n_main_page_247, n_main_page_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода оформление заказа - перход к справке
data2 = n_tutorial_247, n_tutorial_248
data1 = n_cart_247, n_cart_248
display(a_a_test(data1, data2))
после А/В тестов при критическом уровне 0.05 статистически значимых заличий в контрольной выборке и новой нет, но вот при уровне 0.1 тест показывает нам что есть разница в долях при переходе со страницы оформления заказа - справка и оформление заказа - оплата
А/В тест груп общей группы (266+267) и 268
проверяем различие долей для перехода главная страница - страница с продуктами в общей выборке и новой
data2 = n_offer_247+n_offer_246, n_offer_248
data1 = n_main_page_247+n_main_page_246, n_main_page_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с продуктами - оформление заказа
data2 = n_cart_247+n_cart_246, n_cart_248
data1 = n_offer_247+n_offer_246, n_offer_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода страница с оформление заказа - перход к оплате
data2 = n_payment_247+n_payment_246, n_payment_248
data1 = n_cart_247+n_cart_246, n_cart_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода главаня страница - перход к справке общей контрольной и новой
data2 = n_tutorial_247+n_tutorial_246, n_tutorial_248
data1 = n_main_page_247+n_main_page_246, n_main_page_248
display(a_a_test(data1, data2))
проверяем различие долей для перехода оформление - перход к справке общей контрольной и новой
data2 = n_tutorial_247+n_tutorial_246, n_tutorial_248
data1 = n_cart_247+n_cart_246, n_cart_248
display(a_a_test(data1, data2))
после А/В тестов при критическом уровне 0.05 статистически значимых заличий в контрольной выборке и новой нет, но вот при уровне 0.1 тест показывает нам что есть разница в долях при переходе со страницы оформления заказа - оплата
во время тестов критический уровень статистической значимости был 0.05 так и 0.1 и в следующих тестах были такие показатели:
Всего мы провели в общей сложности 40 тестов, 10 А/А тестов и 30 А/В тестов и только 3 из них показали отличия в выборках, что может говорить о том что они ложные (каждые 10 тестов могут иметь ложный результат), но далее об этом в общих выводах
В данной работе мы построили как общую продуктовую воронку так и в разбивке по контрольным группам также провели 40 тестов из которых 10 А/А тестов и 30 А/В тестов и можно сделать такой вывод: